import _typeof from 'babel-runtime/helpers/typeof'
import isPlainObject from 'lodash/isPlainObject'
import { toType, getType, isFunction, validateType, isInteger, isArray, warn } from './utils'

let PropTypes = {
  get any () {
    return toType('any', {
      type: null
    })
  },

  get func () {
    return toType('function', {
      type: Function
    }).def(currentDefaults.func)
  },

  get bool () {
    return toType('boolean', {
      type: Boolean
    }).def(currentDefaults.bool)
  },

  get string () {
    return toType('string', {
      type: String
    }).def(currentDefaults.string)
  },

  get number () {
    return toType('number', {
      type: Number
    }).def(currentDefaults.number)
  },

  get array () {
    return toType('array', {
      type: Array
    }).def(currentDefaults.array)
  },

  get object () {
    return toType('object', {
      type: Object
    }).def(currentDefaults.object)
  },

  get integer () {
    return toType('integer', {
      type: Number,
      validator: function validator (value) {
        return isInteger(value)
      }
    }).def(currentDefaults.integer)
  },

  get symbol () {
    return toType('symbol', {
      type: null,
      validator: function validator (value) {
        return (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'symbol'
      }
    })
  },

  custom: function custom (validatorFn) {
    let warnMsg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'custom validation failed'

    if (typeof validatorFn !== 'function') {
      throw new TypeError('[VueTypes error]: You must provide a function as argument')
    }

    return toType(validatorFn.name || '<<anonymous function>>', {
      validator: function validator () {
        let valid = validatorFn.apply(undefined, arguments)
        if (!valid) warn(this._vueTypes_name + ' - ' + warnMsg)
        return valid
      }
    })
  },
  oneOf: function oneOf (arr) {
    const array = []
    for (let i = 0; i < arguments.length; i++) {
      array.push(arguments[i])
    }
    arr = array
    let msg = 'oneOf - value should be one of "' + arr.join('", "') + '"'
    let allowedTypes = arr.reduce(function (ret, v) {
      if (v !== null && v !== undefined) {
        ret.indexOf(v.constructor) === -1 && ret.push(v.constructor)
      }
      return ret
    }, [])

    return toType('oneOf', {
      type: allowedTypes.length > 0 ? allowedTypes : null,
      validator: function validator (value) {
        let valid = arr.indexOf(value) !== -1
        if (!valid) warn(msg)
        return valid
      }
    })
  },
  instanceOf: function instanceOf (instanceConstructor) {
    return toType('instanceOf', {
      type: instanceConstructor
    })
  },
  oneOfType: function oneOfType (arr) {
    if (!isArray(arr)) {
      throw new TypeError('[VueTypes error]: You must provide an array as argument')
    }

    let hasCustomValidators = false

    let nativeChecks = arr.reduce(function (ret, type) {
      if (isPlainObject(type)) {
        if (type._vueTypes_name === 'oneOf') {
          return ret.concat(type.type || [])
        }
        if (type.type && !isFunction(type.validator)) {
          if (isArray(type.type)) return ret.concat(type.type)
          ret.push(type.type)
        } else if (isFunction(type.validator)) {
          hasCustomValidators = true
        }
        return ret
      }
      ret.push(type)
      return ret
    }, [])

    if (!hasCustomValidators) {
      // we got just native objects (ie: Array, Object)
      // delegate to Vue native prop check
      return toType('oneOfType', {
        type: nativeChecks
      }).def(undefined)
    }

    let typesStr = arr.map(function (type) {
      if (type && isArray(type.type)) {
        return type.type.map(getType)
      }
      return getType(type)
    }).reduce(function (ret, type) {
      return ret.concat(isArray(type) ? type : [type])
    }, []).join('", "')

    return this.custom(function oneOfType (value) {
      let valid = arr.some(function (type) {
        if (type._vueTypes_name === 'oneOf') {
          return type.type ? validateType(type.type, value, true) : true
        }
        return validateType(type, value, true)
      })
      if (!valid) warn('oneOfType - value type should be one of "' + typesStr + '"')
      return valid
    }).def(undefined)
  },
  arrayOf: function arrayOf (type) {
    return toType('arrayOf', {
      type: Array,
      validator: function validator (values) {
        let valid = values.every(function (value) {
          return validateType(type, value)
        })
        if (!valid) warn('arrayOf - value must be an array of "' + getType(type) + '"')
        return valid
      }
    })
  },
  objectOf: function objectOf (type) {
    return toType('objectOf', {
      type: Object,
      validator: function validator (obj) {
        let valid = Object.keys(obj).every(function (key) {
          return validateType(type, obj[key])
        })
        if (!valid) warn('objectOf - value must be an object of "' + getType(type) + '"')
        return valid
      }
    })
  },
  shape: function shape (obj) {
    let keys = Object.keys(obj)
    let requiredKeys = keys.filter(function (key) {
      return obj[key] && obj[key].required === true
    })

    let type = toType('shape', {
      type: Object,
      validator: function validator (value) {
        let _this = this

        if (!isPlainObject(value)) {
          return false
        }
        let valueKeys = Object.keys(value)

        // check for required keys (if any)
        if (requiredKeys.length > 0 && requiredKeys.some(function (req) {
          return valueKeys.indexOf(req) === -1
        })) {
          warn('shape - at least one of required properties "' + requiredKeys.join('", "') + '" is not present')
          return false
        }

        return valueKeys.every(function (key) {
          if (keys.indexOf(key) === -1) {
            if (_this._vueTypes_isLoose === true) return true
            warn('shape - object is missing "' + key + '" property')
            return false
          }
          let type = obj[key]
          return validateType(type, value[key])
        })
      }
    })

    Object.defineProperty(type, '_vueTypes_isLoose', {
      enumerable: false,
      writable: true,
      value: false
    })

    Object.defineProperty(type, 'loose', {
      get: function get () {
        this._vueTypes_isLoose = true
        return this
      },

      enumerable: false
    })

    return type
  }
}

let typeDefaults = function typeDefaults () {
  return {
    func: undefined,
    bool: undefined,
    string: undefined,
    number: undefined,
    array: undefined,
    object: undefined,
    integer: undefined
  }
}

let currentDefaults = typeDefaults()

Object.defineProperty(PropTypes, 'sensibleDefaults', {
  enumerable: false,
  set: function set (value) {
    if (value === false) {
      currentDefaults = {}
    } else if (value === true) {
      currentDefaults = typeDefaults()
    } else if (isPlainObject(value)) {
      currentDefaults = value
    }
  },
  get: function get () {
    return currentDefaults
  }
})

export default PropTypes